package com.ressource;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * This class provides some utilities to launch external applications in a Java
 * program with outputs managed in separate threads.
 */
public class ProcessLauncher
{
	/**
	 * The process object that will launch the command.
	 */
	private Process			process;

	/**
	 * The output stream in which the standard output of the process will be
	 * redirected.
	 */
	private OutputStream	output;

	/**
	 * The output stream in which the error output of the process will be
	 * redirected.
	 */
	private OutputStream	error;

	/**
	 * The input stream which will be redirected to the standard input of the
	 * process.
	 */
	private InputStream		input;

	/**
	 * The time to wait in milliseconds before the thread dies.
	 */
	private long			timeout;

	/**
	 * The state of the process.
	 */
	private boolean			finished	= false;

	/**
	 * Constructs a new process launcher.
	 */
	public ProcessLauncher()
	{
		this(null, null, null, 0L);
	}

	/**
	 * Constructs a new process launcher.
	 * 
	 * @param output
	 *            the output stream where the standard output of the process
	 *            will be written out
	 * @param error
	 *            the output stream where the error output of the process will
	 *            be written out
	 */
	public ProcessLauncher(OutputStream output, OutputStream error)
	{
		this(output, error, null, 0L);
	}

	/**
	 * Constructs a new process launcher.
	 * 
	 * @param output
	 *            the output stream where the standard output of the process
	 *            will be written out
	 * @param error
	 *            the output stream where the error output of the process will
	 *            be written out
	 * @param input
	 *            the input stream where the standard input of the process will
	 *            be written in
	 */
	public ProcessLauncher(OutputStream output, OutputStream error,
			InputStream input)
	{
		this(output, error, input, 0L);
	}

	/**
	 * Constructs a new process launcher.
	 * 
	 * @param output
	 *            the output stream where the standard output of the process
	 *            will be written out
	 * @param error
	 *            the output stream where the error output of the process will
	 *            be written out
	 * @param timeout
	 *            the timeout in milliseconds before the process dies
	 */
	public ProcessLauncher(OutputStream output, OutputStream error, long timeout)
	{
		this(output, error, null, timeout);
	}

	/**
	 * Constructs a new process launcher.
	 * 
	 * @param output
	 *            the output stream where the standard output of the process
	 *            will be written out
	 * @param error
	 *            the output stream where the error output of the process will
	 *            be written out
	 * @param input
	 *            the input stream where the standard input of the process will
	 *            be written in
	 * @param timeout
	 *            the timeout in milliseconds before the process dies
	 */
	public ProcessLauncher(OutputStream output, OutputStream error,
			InputStream input, long timeout)
	{
		this.output = output;
		this.error = error;
		this.input = input;

		this.timeout = timeout < 0. ? 0L : timeout;
	}

	/**
	 * Sets the output stream in which the standard output of the process will
	 * be redirected.
	 * 
	 * @param output
	 *            the output stream for the standard output
	 */
	public void setOutputStream(OutputStream output)
	{
		this.output = output;
	}

	/**
	 * Returns the output stream in which the standard output of the process is
	 * redirected.
	 * 
	 * @return the output stream for the standard output
	 */
	public OutputStream getOutputStream()
	{
		return output;
	}

	/**
	 * Sets the output stream in which the error output of the process will be
	 * redirected.
	 * 
	 * @param error the output stream for the error output
	 */
	public void setErrorStream(OutputStream error)
	{
		this.error = error;
	}

	/**
	 * Returns the output stream in which the error output of the process is
	 * redirected.
	 * 
	 * @return the output stream for the error output
	 */
	public OutputStream getErrorStream()
	{
		return error;
	}

	/**
	 * Sets the input stream which will be redirected to the standard input of
	 * the process.
	 * 
	 * @param input the input stream for the standard input
	 */
	public void setInputStream(InputStream input)
	{
		this.input = input;
	}

	/**
	 * Returns the input stream which is redirected to the standard input of the
	 * process.
	 * 
	 * @return the input stream for the standard input
	 */
	public InputStream getInputStream()
	{
		return input;
	}

	/**
	 * Sets the timeout of the process before it dies.
	 * 
	 * @param timeout
	 *            the timeout in milliseconds
	 */
	public void setTimeout(long timeout)
	{
		this.timeout = timeout;
	}

	/**
	 * Returns the timeout of the process before it dies.
	 * 
	 * @return the timeout in milliseconds
	 */
	public long getTimeout()
	{
		return timeout;
	}

	/**
	 * Launches a command line in a separate thread.
	 * 
	 * @param command
	 *            the command line
	 * @return the exit value for the process
	 * @throws IOException
	 *             if an I/O error occurs while managing the standard input or
	 *             the standard/error output streams
	 * @throws ProcessLauncherException
	 *             if the command failed to be executed
	 */
	public int execute(String command) throws IOException,
			ProcessLauncherException
	{
		try
		{
			process = Runtime.getRuntime().exec(command);
		}
		catch (IOException e)
		{
			/*
			 * In most of the case, this exception is caused by an unknown
			 * command
			 */
			throw new ProcessLauncherException(command);
		}

		return launch();
	}

	/**
	 * Launches a command with its parameters in a separate thread.
	 * 
	 * @param command
	 *            an array containing the command and its parameters
	 * @return the exit value for the process
	 * @throws IOException
	 *             if an I/O error occurs while managing the standard input or
	 *             the standard/error output streams
	 * @throws ProcessLauncherException
	 *             if the command failed to be executed
	 */
	public int execute(String[] command) throws IOException,
			ProcessLauncherException
	{
		try
		{
			process = Runtime.getRuntime().exec(command);
		}
		catch (IOException e)
		{
			/*
			 * In most of the case, this exception is caused by an unknown
			 * command
			 */
			throw new ProcessLauncherException(command[0]);
		}

		return launch();
	}

	/**
	 * Launches a command with its parameters in a separate thread by specifying
	 * environment variables.
	 * 
	 * @param command
	 *            an array containing the command and its parameters
	 * @param environment
	 *            an array containing environment variables definition under the
	 *            form {@code "VARIABLE=VALUE"}
	 * @return the exit value for the process
	 * @throws IOException
	 *             if an I/O error occurs while managing the standard input or
	 *             the standard/error output streams
	 * @throws ProcessLauncherException
	 *             if the command failed to be executed
	 */
	public int execute(String[] command, String[] environment)
			throws IOException, ProcessLauncherException
	{
		try
		{
			process = Runtime.getRuntime().exec(command, environment);
		}
		catch (IOException e)
		{
			/*
			 * In most of the case, this exception is caused by an unknown
			 * command
			 */
			throw new ProcessLauncherException(command[0]);
		}

		return launch();
	}

	/**
	 * Launches a command with its parameters in a separate thread by specifying
	 * environment variables and a working directory.
	 * 
	 * @param command
	 *            an array containing the command and its parameters
	 * @param environment
	 *            an array containing environment variables definition under the
	 *            form {@code VARIABLE=VALUE}
	 * @param directory
	 *            the working directory of the process
	 * @return the exit value for the process
	 * @throws IOException
	 *             if an I/O error occurs while managing the standard input or
	 *             the standard/error output streams
	 * @throws ProcessLauncherException
	 *             if the command failed to be executed
	 */
	public int execute(String[] command, String[] environment, File directory)
			throws IOException, ProcessLauncherException
	{
		try
		{
			process = Runtime.getRuntime()
					.exec(command, environment, directory);
		}
		catch (IOException e)
		{
			/*
			 * In most of the case, this exception is caused by an unknown
			 * command
			 */
			throw new ProcessLauncherException(command[0]);
		}

		return launch();
	}

	/**
	 * Launches a command line in a separate thread by specifying environment
	 * variables.
	 * 
	 * @param command
	 *            the command line
	 * @param environment
	 *            an array containing environment variables definition under the
	 *            form {@code VARIABLE=VALUE}
	 * @return the exit value for the process
	 * @throws IOException
	 *             if an I/O error occurs while managing the standard input or
	 *             the standard/error output streams
	 * @throws ProcessLauncherException
	 *             if the command failed to be executed
	 */
	public int execute(String command, String[] environment)
			throws IOException, ProcessLauncherException
	{
		try
		{
			process = Runtime.getRuntime().exec(command, environment);
		}
		catch (IOException e)
		{
			/*
			 * In most of the case, this exception is caused by an unknown
			 * command
			 */
			throw new ProcessLauncherException(command);
		}

		return launch();
	}

	/**
	 * Launches a command line in a separate thread by specifying environment
	 * variables and a working directory.
	 * 
	 * @param command
	 *            the command line
	 * @param environment
	 *            an array containing environment variables definition under the
	 *            form {@code VARIABLE=VALUE}
	 * @param directory
	 *            the working directory of the process
	 * @return the exit value for the process
	 * @throws IOException
	 *             if an I/O error occurs while managing the standard input or
	 *             the standard/error output streams
	 * @throws ProcessLauncherException
	 *             if the command failed to be executed
	 */
	public int execute(String command, String[] environment, File directory)
			throws IOException, ProcessLauncherException
	{
		try
		{
			process = Runtime.getRuntime()
					.exec(command, environment, directory);
		}
		catch (IOException e)
		{
			/*
			 * In most of the case, this exception is caused by an unknown
			 * command
			 */
			throw new ProcessLauncherException(command);
		}

		return launch();
	}

	/**
	 * Tries to kill the process.
	 */
	public void kill()
	{
		if (process != null)
		{
			process.destroy();
			process = null;
		}
	}

	/**
	 * Launches the process.
	 * 
	 * @return the exit value for the process
	 * @throws IOException
	 *             if an I/O error occurs while managing the standard input or
	 *             the standard/error output streams
	 */
	private int launch() throws IOException
	{
		int status = -1;

		/*
		 * The standard and error outputs and the standard input are consumed in
		 * separate threads to avoid deadlocks
		 */
		if (output == null)
		{
			process.getInputStream().close();
		}
		else
		{
			createStreamThread(process.getInputStream(), output);
		}

		if (error == null)
		{
			process.getErrorStream().close();
		}
		else
		{
			createStreamThread(process.getErrorStream(), error);
		}

		if (input != null)
		{
			createStreamThread(input, process.getOutputStream());
		}

		if (timeout > 0L)
		{
			Thread processThread = createProcessThread(process);
			processThread.start();
			try
			{
				processThread.join(timeout);
				try
				{
					status = process.exitValue();
				}
				catch (IllegalThreadStateException e)
				{
					process.destroy();
					status = process.exitValue();
				}
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
		else
		{
			try
			{
				status = process.waitFor();
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}

		finished = true;

		return status;
	}

	/**
	 * Redirects an input stream to an output stream in a separate thread.
	 * 
	 * @param inputStream
	 *            the input stream to read
	 * @param outputStream
	 *            the output stream on which the input stream {@code
	 *            InputStream} is redirected
	 */
	private void createStreamThread(final InputStream inputStream,
			final OutputStream outputStream)
	{
		new Thread(new Runnable()
		{
			public void run()
			{
				BufferedInputStream bis = new BufferedInputStream(inputStream);
				BufferedOutputStream bos = null;
				if (outputStream != null)
				{
					bos = new BufferedOutputStream(outputStream);
				}

				byte[] d = new byte[2048];
				int r = 0;
				try
				{
					while ((r = bis.read(d)) > 0)
					{
						if (bos != null)
						{
							if (finished)
							{
								break;
							}
							bos.write(d, 0, r);
							bos.flush();
						}
					}
				}
				catch (IOException e)
				{
					e.printStackTrace();
				}
			}
		}).start();
	}

	/**
	 * Creates a separate thread for the specified process.
	 * 
	 * @param process
	 *            the process to be launched in a separate thread
	 * @return the thread in which the process will be launched
	 */
	private Thread createProcessThread(final Process process)
	{
		return new Thread()
		{
			@Override
			public void run()
			{
				try
				{
					process.waitFor();
				}
				catch (InterruptedException ie)
				{
					ie.printStackTrace();
				}
			}
		};
	}
}
